/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.rmi;

import java.awt.*;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderContext;
import java.io.Serializable;
import java.net.InetAddress;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
import java.util.Vector;
import org.eclipse.imagen.*;
import org.eclipse.imagen.media.util.ImageUtil;
import org.eclipse.imagen.remote.RemoteImagingException;
import org.eclipse.imagen.remote.SerializableRenderedImage;
import org.eclipse.imagen.util.ImagingListener;

/* A singleton class representing the serializable version of a null
property. This required because java.awt.Image.UndefinedProperty
is not serializable. */
class NullPropertyTag implements Serializable {
    NullPropertyTag() {}
}

/**
 * The server-side implementation of the RMIImage interface. A RMIImageImpl has a RenderedImage source, acquired via one
 * of three setSource() methods. The first takes a RenderedImage directly as its parameter; this image is simply copied
 * over the network using the normal RMI mechanisms. Note that not every image can be transferred in this way -- for
 * example, attempting to pass an OpImage that uses native code or that depends on the availability of a class not
 * resident on the server as a parameter will cause an exception to be thrown.
 *
 * <p>The second and third ways of setting sources make use of the RenderedOp and RenderableOp classes to send a
 * high-level description of an image chain based on operation names. This chain will be copied over to the server using
 * RMI, where it will be expanded into an OpImage chain using the server's registry. This is the preferred method since
 * it requires less data transfer and offers a better chance of success. It may still fail if the sources or parameters
 * of any operation in the chain are not serializable.
 *
 * <p>RMI requires all remote methods to declare `throws RemoteException' in their signatures. It is up to the client to
 * deal with errors. A simple implementation of error handling may be found in the RemoteRenderedImage class.
 *
 * <p>This class contains a main() method that should be run on the server after starting the RMI registry. The registry
 * will then construct new instances of RMIImageImpl on demand.
 *
 * @see RMIImage
 * @see RemoteImage
 * @see RenderedOp
 * @since EA3
 */
public class RMIImageImpl implements RMIImage {
    /** Tag to represent a null property. */
    public static final Object NULL_PROPERTY = new NullPropertyTag();

    /** Identifier counter for the remote images. */
    private static long idCounter = 0;

    /**
     * The RenderedImage sources hashed by an ID string which must be unique across all possible clients of this object.
     */
    private static Hashtable sources = null;

    /** The PropertySources hashed by an ID string which must be unique across all possible clients of this object. */
    private static Hashtable propertySources = null;

    /**
     * Adds a RenderedImage source to the Hashtable of sources.
     *
     * @param id A unique ID for the source.
     * @param source The source RenderedImage.
     * @param ps The PropertySource.
     */
    private static synchronized void addSource(Long id, RenderedImage source, PropertySource ps) {
        // Create the Hashtables "just in time".
        if (sources == null) {
            sources = new Hashtable();
            propertySources = new Hashtable();
        }

        // Add the source and PropertySource.
        sources.put(id, source);
        propertySources.put(id, ps);
    }

    /**
     * Retrieve a PlanarImage source from the Hashtable of sources.
     *
     * @param id The unique ID of the source.
     * @return The source.
     */
    private static PlanarImage getSource(Long id) throws RemoteException {
        Object obj = null;
        if (sources == null || (obj = sources.get(id)) == null) {
            throw new RemoteException(JaiI18N.getString("RMIImageImpl2"));
        }

        return (PlanarImage) obj;
    }

    /**
     * Retrieve a PropertySource from the Hashtable of PropertySources.
     *
     * @param id The unique ID of the source.
     * @return The PropertySource.
     */
    private static PropertySource getPropertySource(Long id) throws RemoteException {
        Object obj = null;
        if (propertySources == null || (obj = propertySources.get(id)) == null) {
            throw new RemoteException(JaiI18N.getString("RMIImageImpl2"));
        }

        return (PropertySource) obj;
    }

    /** Constructs a RMIImageImpl with a source to be specified later. */
    public RMIImageImpl() throws RemoteException {
        super();
        try {
            UnicastRemoteObject.exportObject(this);
        } catch (RemoteException e) {
            ImagingListener listener = ImageUtil.getImagingListener((RenderingHints) null);
            String message = JaiI18N.getString("RMIImageImpl0");
            listener.errorOccurred(message, new RemoteImagingException(message, e), this, false);
            /*
                        e.printStackTrace();
                        throw new RuntimeException(JaiI18N.getString("RMIImageImpl0") +
                                                   e.getMessage());
            */
        }
    }

    /**
     * Returns the identifier of the remote image. This method should be called to return an identifier before any other
     * methods are invoked. The same ID must be used in all subsequent references to the remote image.
     */
    public synchronized Long getRemoteID() throws RemoteException {
        return new Long(++idCounter);
    }

    /**
     * Sets the source of the image on the server side. This source should ideally be a lightweight reference to an
     * image available locally on the server or over a further network link (for example, an IIPOpImage that contains a
     * URL but not actual image data).
     *
     * <p>Although it is legal to use any RenderedImage, one should be aware that a deep copy might be made and
     * transmitted to the server.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param source a RenderedImage source.
     */
    public void setSource(Long id, RenderedImage source) throws RemoteException {
        PlanarImage pi = PlanarImage.wrapRenderedImage(source);
        addSource(id, pi, pi);
    }

    /**
     * Sets the source to a RenderedOp (i.e., an imaging DAG). This DAG will be copied over to the server where it will
     * be transformed into an OpImage chain using the server's local OperationRegistry and available
     * RenderedImageFactory objects.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param source a RenderedOp source.
     */
    public void setSource(Long id, RenderedOp source) throws RemoteException {
        addSource(id, source.getRendering(), source);
    }

    /**
     * Sets the source to a RenderableOp defined by a renderable imaging DAG and a rendering context. The entire
     * RenderableImage DAG will be copied over to the server.
     */
    public void setSource(Long id, RenderableOp source, RenderContextProxy renderContextProxy) throws RemoteException {
        RenderContext renderContext = renderContextProxy.getRenderContext();
        RenderedImage r = source.createRendering(renderContext);
        PlanarImage pi = PlanarImage.wrapRenderedImage(r);
        addSource(id, pi, pi);
    }

    /** Disposes of any resouces allocated to the client object with the specified ID. */
    public void dispose(Long id) throws RemoteException {
        if (sources != null) {
            sources.remove(id);
            propertySources.remove(id);
        }
    }

    /**
     * Gets a property from the property set of this image. If the property is undefined the constant NULL_PROPERTY is
     * returned.
     */
    public Object getProperty(Long id, String name) throws RemoteException {
        PropertySource ps = getPropertySource(id);
        Object property = ps.getProperty(name);
        if (property == null || property.equals(java.awt.Image.UndefinedProperty)) {
            property = NULL_PROPERTY;
        }
        return property;
    }

    /**
     * Returns a list of names recognized by getProperty().
     *
     * @return an array of Strings representing proeprty names.
     */
    public String[] getPropertyNames(Long id) throws RemoteException {
        PropertySource ps = getPropertySource(id);
        return ps.getPropertyNames();
    }

    /** Returns the minimum X coordinate of the RMIImage. */
    public int getMinX(Long id) throws RemoteException {
        return getSource(id).getMinX();
    }

    /** Returns the smallest X coordinate to the right of the RMIImage. */
    public int getMaxX(Long id) throws RemoteException {
        return getSource(id).getMaxX();
    }

    /** Returns the minimum Y coordinate of the RMIImage. */
    public int getMinY(Long id) throws RemoteException {
        return getSource(id).getMinY();
    }

    /** Returns the smallest Y coordinate below the RMIImage. */
    public int getMaxY(Long id) throws RemoteException {
        return getSource(id).getMaxY();
    }

    /** Returns the width of the RMIImage. */
    public int getWidth(Long id) throws RemoteException {
        return getSource(id).getWidth();
    }

    /** Returns the height of the RMIImage. */
    public int getHeight(Long id) throws RemoteException {
        return getSource(id).getHeight();
    }

    /** Returns the width of a tile in pixels. */
    public int getTileWidth(Long id) throws RemoteException {
        return getSource(id).getTileWidth();
    }

    /** Returns the height of a tile in pixels. */
    public int getTileHeight(Long id) throws RemoteException {
        return getSource(id).getTileHeight();
    }

    /** Returns the X coordinate of the upper-left pixel of tile (0, 0). */
    public int getTileGridXOffset(Long id) throws RemoteException {
        return getSource(id).getTileGridXOffset();
    }

    /** Returns the Y coordinate of the upper-left pixel of tile (0, 0). */
    public int getTileGridYOffset(Long id) throws RemoteException {
        return getSource(id).getTileGridYOffset();
    }

    /** Returns the index of the leftmost column of tiles. */
    public int getMinTileX(Long id) throws RemoteException {
        return getSource(id).getMinTileX();
    }

    /** Returns the number of tiles along the tile grid in the horizontal direction. */
    public int getNumXTiles(Long id) throws RemoteException {
        return getSource(id).getNumXTiles();
    }

    /** Returns the index of the uppermost row of tiles. */
    public int getMinTileY(Long id) throws RemoteException {
        return getSource(id).getMinTileY();
    }

    /** Returns the number of tiles along the tile grid in the vertical direction. */
    public int getNumYTiles(Long id) throws RemoteException {
        return getSource(id).getNumYTiles();
    }

    /** Returns the index of the rightmost column of tiles. */
    public int getMaxTileX(Long id) throws RemoteException {
        return getSource(id).getMaxTileX();
    }

    /** Returns the index of the bottom row of tiles. */
    public int getMaxTileY(Long id) throws RemoteException {
        return getSource(id).getMaxTileY();
    }

    /** Returns the SampleModel associated with this image. */
    public SampleModelProxy getSampleModel(Long id) throws RemoteException {
        return new SampleModelProxy(getSource(id).getSampleModel());
    }

    /** Returns the ColorModel associated with this image. */
    public ColorModelProxy getColorModel(Long id) throws RemoteException {
        return new ColorModelProxy(getSource(id).getColorModel());
    }

    /**
     * Returns a vector of RenderedImages that are the sources of image data for this RMIImage. Note that this method
     * will often return an empty vector.
     */
    public Vector getSources(Long id) throws RemoteException {
        Vector sourceVector = getSource(id).getSources();
        int size = sourceVector.size();
        boolean isCloned = false;
        for (int i = 0; i < size; i++) {
            RenderedImage img = (RenderedImage) sourceVector.get(i);
            if (!(img instanceof Serializable)) {
                if (!isCloned) {
                    sourceVector = (Vector) sourceVector.clone();
                }
                sourceVector.set(i, new SerializableRenderedImage(img, false));
            }
        }
        return sourceVector;
    }

    /** Returns a Rectangle indicating the image bounds. */
    public Rectangle getBounds(Long id) throws RemoteException {
        return getSource(id).getBounds();
    }

    /**
     * Returns tile (x, y). Note that x and y are indices into the tile array, not pixel locations. Unlike in the true
     * RenderedImage interface, the Raster that is returned should be considered a copy.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param tileX the X index of the requested tile in the tile array.
     * @param tileY the Y index of the requested tile in the tile array.
     * @return the tile as a Raster.
     */
    public RasterProxy getTile(Long id, int tileX, int tileY) throws RemoteException {
        return new RasterProxy(getSource(id).getTile(tileX, tileY));
    }

    /**
     * Returns the entire image as a single Raster.
     *
     * @return a Raster containing a copy of this image's data.
     */
    public RasterProxy getData(Long id) throws RemoteException {
        return new RasterProxy(getSource(id).getData());
    }

    /**
     * Returns an arbitrary rectangular region of the RenderedImage in a Raster. The rectangle of interest will be
     * clipped against the image bounds.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param rect the region of the RenderedImage to be returned.
     * @return a Raster containing a copy of the desired data.
     */
    public RasterProxy getData(Long id, Rectangle bounds) throws RemoteException {
        RasterProxy rp = null;
        if (bounds == null) {
            rp = getData(id);
        } else {
            bounds = bounds.intersection(getBounds(id));
            rp = new RasterProxy(getSource(id).getData(bounds));
        }
        return rp;
    }

    /** Returns the same result as getData(Rectangle) would for the same rectangular region. */
    public RasterProxy copyData(Long id, Rectangle bounds) throws RemoteException {
        return getData(id, bounds);
    }

    /**
     * Starts a server on a given port. The RMI registry must be running on the server host.
     *
     * <p>The usage of this class is
     *
     * <pre>
     * java -Djava.rmi.server.codebase=file:$JAI/lib/jai.jar \
     * -Djava.rmi.server.useCodebaseOnly=false \
     * -Djava.security.policy=\
     * file:`pwd`/policy org.eclipse.imagen.media.rmi.RMIImageImpl \
     * [-host hostName] [-port portNumber]
     * </pre>
     *
     * The default host is the local host and the default port is 1099.
     *
     * @param args the port number as a command-line argument.
     */
    public static void main(String[] args) {
        // Set the security manager.
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }

        // Set the host name and port number.
        String host = null;
        int port = 1099; // default port is 1099
        for (int i = 0; i < args.length; i++) {
            if (args[i].equalsIgnoreCase("-host")) {
                host = args[++i];
            } else if (args[i].equalsIgnoreCase("-port")) {
                port = Integer.parseInt(args[++i]);
            }
        }

        // Default to the local host if the host was not specified.
        if (host == null) {
            try {
                host = InetAddress.getLocalHost().getHostAddress();
            } catch (java.net.UnknownHostException e) {
                System.err.println(JaiI18N.getString("RMIImageImpl1") + e.getMessage());
                e.printStackTrace();
            }
        }

        System.out.println(JaiI18N.getString("RMIImageImpl3") + " " + host + ":" + port);

        try {
            RMIImageImpl im = new RMIImageImpl();
            String serverName = new String("rmi://" + host + ":" + port + "/" + RMIImage.RMI_IMAGE_SERVER_NAME);
            System.out.println(JaiI18N.getString("RMIImageImpl4") + " \"" + serverName + "\".");
            Naming.rebind(serverName, im);
            System.out.println(JaiI18N.getString("RMIImageImpl5"));
        } catch (Exception e) {
            System.err.println(JaiI18N.getString("RMIImageImpl0") + e.getMessage());
            e.printStackTrace();
        }
    }
}
